From 4a9bf575bd24b2e24348c65d9fbbf9c81a73cb35 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Tue, 30 Jan 2024 16:36:29 -0500 Subject: Create expense from receipt (#69) * Create expense from receipt * Add modal * Update README --- .../expenses/create-from-receipt-button.tsx | 278 +++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx (limited to 'src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx') diff --git a/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx b/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx new file mode 100644 index 0000000..5feb931 --- /dev/null +++ b/src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx @@ -0,0 +1,278 @@ +'use client' + +import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' +import { + ReceiptExtractedInfo, + extractExpenseInformationFromImage, +} from '@/app/groups/[groupId]/expenses/create-from-receipt-button-actions' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/components/ui/drawer' +import { ToastAction } from '@/components/ui/toast' +import { useToast } from '@/components/ui/use-toast' +import { useMediaQuery } from '@/lib/hooks' +import { formatExpenseDate } from '@/lib/utils' +import { Category } from '@prisma/client' +import { ChevronRight, Loader2, Receipt } from 'lucide-react' +import { getImageData, useS3Upload } from 'next-s3-upload' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import { PropsWithChildren, ReactNode, useState } from 'react' + +type Props = { + groupId: string + groupCurrency: string + categories: Category[] +} + +export function CreateFromReceiptButton({ + groupId, + groupCurrency, + categories, +}: Props) { + const [pending, setPending] = useState(false) + const { uploadToS3, FileInput, openFileDialog } = useS3Upload() + const { toast } = useToast() + const router = useRouter() + const [receiptInfo, setReceiptInfo] = useState< + | null + | (ReceiptExtractedInfo & { url: string; width?: number; height?: number }) + >(null) + const isDesktop = useMediaQuery('(min-width: 640px)') + + const handleFileChange = async (file: File) => { + const upload = async () => { + try { + setPending(true) + console.log('Uploading image…') + let { url } = await uploadToS3(file) + console.log('Extracting information from receipt…') + const { amount, categoryId, date, title } = + await extractExpenseInformationFromImage(url) + const { width, height } = await getImageData(file) + setReceiptInfo({ amount, categoryId, date, title, url, width, height }) + } catch (err) { + console.error(err) + toast({ + title: 'Error while uploading document', + description: + 'Something wrong happened when uploading the document. Please retry later or select a different file.', + variant: 'destructive', + action: ( + upload()}> + Retry + + ), + }) + } finally { + setPending(false) + } + } + upload() + } + + const receiptInfoCategory = + (receiptInfo?.categoryId && + categories.find((c) => String(c.id) === receiptInfo.categoryId)) || + null + + const DialogOrDrawer = isDesktop + ? CreateFromReceiptDialog + : CreateFromReceiptDrawer + + return ( + + + + } + title={ + <> + Create from receipt + + Beta + + + } + description={<>Extract the expense information from a receipt photo.} + > +
+

+ Upload the photo of a receipt, and we’ll scan it to extract the + expense information if we can. +

+
+ +
+ +
+ Title: +
{receiptInfo?.title ?? '…'}
+
+
+ Category: +
+ {receiptInfoCategory ? ( +
+ + {receiptInfoCategory.grouping} + + {receiptInfoCategory.name} +
+ ) : ( + '' || '…' + )} +
+
+
+ Amount: +
+ {receiptInfo?.amount ? ( + <> + {groupCurrency} {receiptInfo.amount.toFixed(2)} + + ) : ( + '…' + )} +
+
+
+ Date: +
+ {receiptInfo?.date + ? formatExpenseDate( + new Date(`${receiptInfo?.date}T12:00:00.000Z`), + ) + : '…'} +
+
+
+
+

You’ll be able to edit the expense information after creating it.

+
+ +
+
+
+ ) +} + +function CreateFromReceiptDialog({ + trigger, + title, + description, + children, +}: PropsWithChildren<{ + trigger: ReactNode + title: ReactNode + description: ReactNode +}>) { + return ( + + {trigger} + + + {title} + + {description} + + + {children} + + + ) +} + +function CreateFromReceiptDrawer({ + trigger, + title, + description, + children, +}: PropsWithChildren<{ + trigger: ReactNode + title: ReactNode + description: ReactNode +}>) { + return ( + + {trigger} + + + {title} + + {description} + + +
{children}
+
+
+ ) +} -- cgit v1.3